package model

import (
	"github.com/aberic/gnomon"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v3"
	proto "gykjgit.dccnet.com.cn/chain/proto/swarm"
)

// Compose stack启动配置文件信息
type Compose struct {
	Version  string              `yaml:"version"`            // yaml版本号，默认3.8
	Services map[string]*Task    `yaml:"services"`           // 启动期望服务集合
	Networks map[string]*Network `yaml:"networks,omitempty"` // 启动所需网络集合
}

// NewCompose NewCompose
func NewCompose(compose *proto.Compose) (*Compose, error) {
	services := map[string]*Task{}
	if nil == compose.Services {
		return nil, errors.New("services can not be nil")
	}
	for containerName, container := range compose.Services {
		c, err := newContainer(container)
		if nil != err {
			return nil, errors.Wrap(err, "container init error")
		}
		services[containerName] = c
	}
	if nil == compose.Networks {
		return nil, errors.New("networks can not be nil")
	}
	networks := map[string]*Network{}
	for networkName, network := range compose.Networks {
		networks[networkName] = newNetwork(network)
	}
	return &Compose{Version: "3.8", Services: services, Networks: networks}, nil
}

// WriteTo WriteTo
func (c *Compose) WriteTo(filepath string) error {
	bs, err := yaml.Marshal(c)
	if nil != err {
		return err
	}
	_, err = gnomon.FileAppend(filepath, bs, true)
	return err
}

// Container 容器服务
type Task struct {
	Image       string   `yaml:"image,omitempty"`       // 镜像服务
	Ports       []string `yaml:"ports,omitempty"`       // 端口映射策略集合
	Environment []string `yaml:"environment,omitempty"` // 环境变量
	Commands    []string `yaml:"-"`                     // 启动容器后执行的默认命令
	Args        []string `yaml:"-"`                     // 启动容器后执行参数
	Command     string   `yaml:"command,omitempty"`     // 启动容器后执行的默认命令
	WorkingDir  string   `yaml:"working_dir,omitempty"` // 容器内部服务工作目录
	Volumes     []string `yaml:"volumes,omitempty"`     // 挂载服务集合
	Networks    []string `yaml:"networks,omitempty"`    // 工作网络集合
	// 通常，当前台进程在docker容器内完成时，它会假设作业已经完成并清理容器实例。
	// 但是数据库引擎和web服务器等需要继续运行。
	// 通过创建一个伪终端，容器保持活动状态。
	// 可以通过运行docker logs <<container name>>命令(带tty和不带tty)来查看这个工作流
	TTY        bool     `yaml:"tty,omitempty"`
	ExtraHosts []string `yaml:"extra_hosts,omitempty"` // dns服务集合
	DependsOn  []string `yaml:"depends_on,omitempty"`  // 依赖关系服务
	Deploy     *Deploy  `yaml:"deploy,omitempty"`      // 指定swarm服务部署和运行时的相关配置
}

func newContainer(task *proto.Task) (*Task, error) {
	if nil == task {
		return nil, errors.New("container can not be nil")
	}
	var ports, environments, volumes, extraHosts []string
	if nil != task.Ports {
		for _, port := range task.Ports {
			ports = append(ports, gnomon.StringBuild(port.Publish, ":", port.Target))
		}
	}
	if nil != task.Environments {
		for _, env := range task.Environments {
			environments = append(environments, gnomon.StringBuild(env.Key, "=", env.Value))
		}
	}
	if nil != task.Volumes {
		for _, volume := range task.Volumes {
			volumes = append(volumes, gnomon.StringBuild(volume.Local, ":", volume.Mount))
		}
	}
	if nil != task.ExtraHosts {
		for _, host := range task.ExtraHosts {
			extraHosts = append(extraHosts, gnomon.StringBuild(host.Host, ":", host.HostMap))
		}
	}
	c := &Task{
		Image:       gnomon.StringBuild(task.Image.Name, ":", task.Image.Version),
		Ports:       ports,
		Environment: environments,
		Command:     gnomon.StringBuildSep(" ", task.Command...),
		Commands:    task.Command,
		WorkingDir:  task.WorkingDir,
		Volumes:     volumes,
		Networks:    task.Networks,
		TTY:         task.TTY,
		ExtraHosts:  extraHosts,
		DependsOn:   task.DependsOn,
	}
	if nil != task.Deploy {
		c.Deploy = newDeploy(task.Deploy)
	}
	return c, nil
}

// Network swarm独立网络
type Network struct {
	// 网络驱动类型：host、bridge、overlay
	Driver     string `yaml:"driver,omitempty"`
	Attachable bool   `yaml:"attachable,omitempty"`
}

func newNetwork(network *proto.ComposeNetwork) *Network {
	return &Network{Driver: networkDriver(network.Driver), Attachable: network.Attachable}
}

func networkDriver(driver proto.Driver) string {
	switch driver {
	default:
		return ""
	case proto.Driver_host:
		return "host"
	case proto.Driver_bridge:
		return "bridge"
	case proto.Driver_overlay:
		return "overlay"
	}
}

// 指定swarm服务部署和运行时的相关配置
type Deploy struct {
	Mode     string `yaml:"mode,omitempty"`     // 全局global（每个群集节点只有一个容器）或副本replicated（指定容器的数量）。默认值被副本
	Replicas uint64 `yaml:"replicas,omitempty"` // 如果服务是副本模式（默认模式），可以指定该服务运行的容器数量
	// 指定swarm服务发现的模式
	//
	// endpoint_mode: vip - Docker为swarm集群服务分配一个虚拟IP(VIP)，作为客户端到达集群服务的“前端”。
	// Docker 在客户端和可用工作节点之间对服务的请求进行路由。而客户端不用知道有多少节点参与服务或者是这些节点的IP/端口。（这是默认模式）
	//
	// endpoint_mode: dnsrr - DNS轮询（DNSRR）服务发现不使用单个虚拟IP。
	// Docker为服务设置DNS条目，使得服务名称的DNS查询返回一个IP地址列表，并且客户端直接连接到其中的一个。
	// 如果想使用自己的负载平衡器，或者混合Windows和Linux应用程序，则DNS轮询功能非常有用。
	EndpointMode   string            `yaml:"endpoint_mode,omitempty"`
	Resources      *Resources        `yaml:"resources,omitempty"`       // 资源配置策略
	Labels         map[string]string `yaml:"labels,omitempty"`          // 指定服务的标签。这些标签仅在服务上设置，而不在服务的任何容器上设置
	Placement      *Placement        `yaml:"placement,omitempty"`       // 指定约束和偏好设置
	RestartPolicy  *RestartPolicy    `yaml:"restart_policy,omitempty"`  // 配置在容器退出时是否并如何重启容器。取代restart指令
	UpdateConfig   *UpdateConfig     `yaml:"update_config,omitempty"`   // 配置服务如何升级
	RollbackConfig *RollbackConfig   `yaml:"rollback_config,omitempty"` // 配置服务如何回滚
}

func newDeploy(deploy *proto.Deploy) *Deploy {
	var (
		replicas uint64
	)
	if deploy.Replicas < 1 {
		replicas = 1
	} else {
		replicas = deploy.Replicas
	}
	d := &Deploy{
		Replicas: replicas,
		Labels:   deploy.Labels,
	}
	if nil != deploy.Placement {
		d.Placement = newPlacement(deploy.Placement)
	}
	if nil != deploy.Resources {
		d.Resources = newResources(deploy.Resources)
	}
	if mode := containerMode(deploy.Mode); gnomon.StringIsNotEmpty(mode) {
		d.Mode = mode
	}
	if mode := containerEndpointMode(deploy.EndpointMode); gnomon.StringIsNotEmpty(mode) {
		d.EndpointMode = mode
	}
	if nil != deploy.RestartPolicy {
		d.RestartPolicy = newRestartPolicy(deploy.RestartPolicy)
	}
	if nil != deploy.UpdateConfig {
		d.UpdateConfig = newUpdateConfig(deploy.UpdateConfig)
	}
	if nil != deploy.RollbackConfig {
		d.RollbackConfig = newRollbackConfig(deploy.RollbackConfig)
	}
	return d
}

func containerMode(mode proto.Mode) string {
	switch mode {
	default:
		return "replicated"
	case proto.Mode_global:
		return "global"
	case proto.Mode_replicated:
		return "replicated"
	}
}

func containerEndpointMode(mode proto.EndpointMode) string {
	switch mode {
	default:
		return "vip"
	case proto.EndpointMode_vip:
		return "vip"
	case proto.EndpointMode_dnsrr:
		return "dnsrr"
	}
}

func containerRestartCondition(policy proto.Condition) string {
	switch policy {
	default:
		return "any"
	case proto.Condition_none:
		return "none"
	case proto.Condition_any:
		return "any"
	case proto.Condition_onFailure:
		return "on-failure"
	}
}

func containerAction(action proto.Action) string {
	switch action {
	default:
		return "rollback"
	case proto.Action_continue:
		return "continue"
	case proto.Action_rollback:
		return "rollback"
	case proto.Action_pause:
		return "pause"
	}
}

func containerOrder(order proto.Order) string {
	switch order {
	default:
		return "stop-first"
	case proto.Order_stopFirst:
		return "stop-first"
	case proto.Order_startFirst:
		return "start-first"
	}
}

// Resources 资源配置策略
type Resources struct {
	Limits       *Resource `yaml:"limits,omitempty"`       // 限制使用不超过资源
	Reservations *Resource `yaml:"reservations,omitempty"` // 总是可用资源
}

func newResources(resources *proto.Resources) *Resources {
	r := &Resources{}
	if nil != resources.Limits {
		r.Limits = &Resource{
			CPUs:   resources.Limits.Cpus,
			Memory: resources.Limits.Memory,
		}
	}
	if nil != resources.Reservations {
		r.Reservations = &Resource{
			CPUs:   resources.Reservations.Cpus,
			Memory: resources.Reservations.Memory,
		}
	}
	return r
}

// Resource 资源配置内容
type Resource struct {
	CPUs   string `yaml:"cpus,omitempty"`   // cpu比例，如0.5表示50％
	Memory string `yaml:"memory,omitempty"` // 内存值，如50M
}

// Placement 指定约束和偏好设置
type Placement struct {
	// 约定服务运行位置
	//
	// node.role==manager
	//
	// node.hostname == test1.spuddy.org
	//
	// node.labels.Tomcat == true
	//
	// engine.labels.operatingsystem==ubuntu 18.04
	Constraints []string `yaml:"constraints,omitempty"`
	// 可以通过设置服务将任务均匀地划分到不同类别的节点上
	//
	// 如在一组数据中心或可用性区域上平衡任务
	//
	// spread=node.labels.datacenter
	Preferences []*Preference `yaml:"preferences,omitempty"`
}

func newPlacement(placement *proto.Placement) *Placement {
	var (
		preferences []*Preference
		constraints []string
	)
	for _, p := range placement.Preferences {
		if nil != p {
			preferences = append(preferences, &Preference{Spread: p.Spread})
		}
	}
	if nil != placement.Constraints {
		constraints = placement.Constraints
	}
	return &Placement{
		Constraints: constraints,
		Preferences: preferences,
	}

}

// Preference 可以通过设置服务将任务均匀地划分到不同类别的节点上
type Preference struct {
	Spread string `yaml:"spread,omitempty"` // 均匀传播的对象属性
}

// RestartPolicy 配置在容器退出时是否并如何重启容器。取代restart指令
type RestartPolicy struct {
	Condition   string `yaml:"condition,omitempty"`    // 重启判定条件，none、on-failure和any（默认any）
	Delay       string `yaml:"delay,omitempty"`        // 在重启尝试之间等待多久（默认0）
	MaxAttempts uint64 `yaml:"max_attempts,omitempty"` // 尝试重启的次数（默认一直重启，直到成功）
	Window      string `yaml:"window,omitempty"`       // 在确实一个重启是否成功前需要等待的窗口时间
}

func newRestartPolicy(restartPolicy *proto.RestartPolicy) *RestartPolicy {
	r := &RestartPolicy{
		Delay:       restartPolicy.Delay,
		MaxAttempts: restartPolicy.MaxAttempts,
		Window:      restartPolicy.Window,
	}
	if condition := containerRestartCondition(restartPolicy.Condition); gnomon.StringIsNotEmpty(condition) {
		r.Condition = condition
	}
	return r
}

// UpdateConfig 配置服务如何升级
type UpdateConfig struct {
	Parallelism     uint64  `yaml:"parallelism,omitempty"`       // 同一时间升级的容器数量
	Delay           string  `yaml:"delay,omitempty"`             // 容器升级间隔时间
	FailureAction   string  `yaml:"failure_action,omitempty"`    // 升级失败后的动作（continue、rollback和pause。默认pause）
	Monitor         string  `yaml:"monitor,omitempty"`           // 更新完成后确实成功的时间（ns|us|ms|s|m|h）。（默认0s）
	MaxFailureRatio float32 `yaml:"max_failure_ratio,omitempty"` // 更新期间允许的失败率
	// 更新期间的操作顺序。停止优先（旧任务在开始新任务之前停止）或者先启动（首先启动新任务，并且正在运行的任务短暂重叠）
	//
	// stop-first/start-first
	//
	//（默认停止优先）注意：只支持v3.4及更高版本
	Order string `yaml:"order,omitempty"`
}

func newUpdateConfig(updateConfig *proto.UpdateConfig) *UpdateConfig {
	u := &UpdateConfig{
		Parallelism:     updateConfig.Parallelism,
		Delay:           updateConfig.Delay,
		Monitor:         updateConfig.Monitor,
		MaxFailureRatio: updateConfig.MaxFailureRatio,
	}
	if action := containerAction(updateConfig.FailureAction); gnomon.StringIsNotEmpty(action) {
		u.FailureAction = action
	}
	if order := containerOrder(updateConfig.Order); gnomon.StringIsNotEmpty(order) {
		u.Order = order
	}
	return u
}

// RollbackConfig 配置在更新失败的情况下应如何回滚服务
type RollbackConfig struct {
	Parallelism     uint64  `yaml:"parallelism,omitempty"`       // 一次回滚的容器数。如果设置为0，则所有容器同时回滚
	Delay           string  `yaml:"delay,omitempty"`             // 每个容器组的回滚之间等待的时间（默认为0）
	FailureAction   string  `yaml:"failure_action,omitempty"`    // 回滚失败后操作动作。一个continue或pause（默认pause）
	Monitor         string  `yaml:"monitor,omitempty"`           // 每次更新任务后的持续时间以监视失败(ns|us|ms|s|m|h)（默认为0）
	MaxFailureRatio float32 `yaml:"max_failure_ratio,omitempty"` // 回滚期间容忍的失败率（默认值为0）
	// 回滚期间的操作顺序。其中之一stop-first（旧任务在启动新任务之前停止），或者start-first（首先启动新任务，并且正在运行的任务暂时重叠）
	//
	// stop-first/start-first
	//
	//（默认停止优先）注意：只支持v3.4及更高版本
	Order string `yaml:"order,omitempty"`
}

func newRollbackConfig(rollbackConfig *proto.UpdateConfig) *RollbackConfig {
	r := &RollbackConfig{
		Parallelism:     rollbackConfig.Parallelism,
		Delay:           rollbackConfig.Delay,
		Monitor:         rollbackConfig.Monitor,
		MaxFailureRatio: rollbackConfig.MaxFailureRatio,
	}
	if action := containerAction(rollbackConfig.FailureAction); gnomon.StringIsNotEmpty(action) && action != "rollback" {
		r.FailureAction = action
	}
	if order := containerOrder(rollbackConfig.Order); gnomon.StringIsNotEmpty(order) {
		r.Order = order
	}
	return r
}
